How FriendFeed uses MySQL to store schema-less data
FriendFeedは、どのように、スキーマのないデータを格納するために、MySQLを使いますか
ブレットテイラーによって · 2009年2月27日
Background
背景
We use MySQL for storing all of the data in FriendFeed.
Our database has grown a lot as our user base has grown. We now store
over 250 million entries and a bunch of other data, from comments and
"likes" to friend lists.
我々は、データの全てをFriendFeedに保存するために、MySQLを使います。我々のユーザー主成分が成長して、我々のデータベースはたいへん成長しました。コメントと「好み」から友人リストまで、我々は現在2億5000万以上のエントリとたくさんの他のデータを格納します。
As our database has grown, we have tried to iteratively deal with
the scaling issues that come with rapid growth. We did the typical
things, like using read slaves and memcache to increase read throughput
and sharding our database to improve write throughput. However, as we
grew, scaling our existing features to accomodate more traffic turned
out to be much less of an issue than adding new features.
我
々のデータベースが成長して、我々はiterativelyに急成長とともに来るスケーリング問題に対処しようとしました。我々は典型的ことをしました、
読まれたスループットを増やすために読まれた奴隷とmemcacheを使って、よくなるために我々のデータベースをshardingすることの様に、ス
ループットを書いてください。しかし、我々が成長したので、より多くの交通をaccomodateするために我々の既存の特徴を登ることは問題の加わるこ
とより非常に少ないことがわかりました 新機能。
In particular, making schema changes or adding indexes to a database
with more than 10 - 20 million rows completely locks the database for
hours at a time. Removing old indexes takes just as much time, and not
removing them hurts performance because the database will continue to
read and write to those unused blocks on every
特
に、スキーマ変化をもたらすか、インデックスをデータベースに10以上と加えます -
2000万は、完全にロックをこぎます何時間も一度にデータベース。古いインデックスを削除することはちょうど同じくらいの時間をとります、そして、デー
タベースがあらゆるものの上でそれらの使っていないブロックに読み書きし続けるので、彼らを追い出さないことはパフォーマンスを傷つけます INSERT,
pushing important blocks out of memory. There are complex operational
procedures you can do to circumvent these problems (like setting up the
new index on a slave, and then swapping the slave and the master), but
those procedures are so error prone and heavyweight, they implicitly
discouraged our adding features that would require schema/index
changes. Since our databases are all heavily sharded, the relational
features of MySQL like
挿入
記
憶から重要なブロックを押すこと。あなたがこれらの問題(奴隷の上で新しいインデックスを準備して、それから、奴隷とマスターを交換することのように)を
回避するためにすることができる複雑な操作上の手順があります、しかし、それらの手順はそううつ伏せのエラーとヘビー級です、彼らは暗黙のうちに、スキー
マ/インデックス変化を必要とする我々の追加特徴をはばみました。我々のデータベースがMySQLのすべてのかなりshardedされた、関係の特徴であ
るので、好きにしてください JOIN have never been useful to us, so we decided to look outside of the realm of RDBMS.
参加...
我々にとってこれまで有用にしてはいけないので、我々はRDBMSの領域の外で見ることに決めました。
Lots of projects exist designed to tackle the problem storing data
with flexible schemas and building new indexes on the fly (e.g., CouchDB).
However, none of them seemed widely-used enough by large sites to
inspire confidence. In the tests we read about and ran ourselves, none
of the projects were stable or battle-tested enough for our needs (see this somewhat outdated article on CouchDB,
for example). MySQL works. It doesn't corrupt data. Replication works.
We understand its limitations already. We like MySQL for storage, just
not RDBMS usage patterns.
たくさんのプロジェクトが、柔軟なスキーマをデータにたくわえていて、自動的にその場で(例えばCouchDB)
新しいインデックスを構築している問題に取り組むようにできて存在します。しかし、彼らの誰も、信頼を吹き込むために、広い敷地によって十分に広く使われ
ているようでありませんでした。我々が読んで、我々自身走らせたテストでは、プロジェクトのどれも、安定でなかったか、我々のニーズ(たとえば、CouchDBに関するこのいくぶん時代遅れの記事を見てください)のために、十分に戦いの検査済みでありませんでした。MySQLは働きます。それは、データを壊しません。複製は働きます。我々は、すでにその限界を理解します。我々は、保管(ちょっとRDBMS使用パターンでない)のために、MySQLが好きです。
After some deliberation, we decided to implement a "schema-less"
storage system on top of MySQL rather than use a completely new storage
system. This post attempts to describe the high-level details of the
system. We are curious how other large sites have tackled these
problems, and we thought some of the design work we have done might be
useful to other developers.
い
くらかの熟考の後、我々は完全に新しい記憶装置を使うよりはむしろ、MySQLの上で「スキーマのない」記憶装置を実装することに決めました。このポスト
は、システムの高水準詳細を記述しようとします。我々は他の広い敷地がどのようにこれらの問題に取り組んだかについて知りたいです、そして、我々は我々が
したデザイン作業のいくつかが他の開発者にとって有用かもしれないと思いました。
Overview
概要
Our datastore stores schema-less bags of properties (e.g., JSON
objects or Python dictionaries). The only required property of stored
entities is
我々のデータストアは、特性(例えばJSON物またはパイソン辞書)のスキーマのないバッグを保存します。保存された実体の唯一の必須の財産は、そうです id,
a 16-byte UUID. The rest of the entity is opaque as far as the
datastore is concerned. We can change the "schema" simply by storing
new properties.
ID
16バイトのUUID。データストアが関係する限り、残りの実体は不透明です。我々は、単に新しい特性を保存することによって、「スキーマ」を変えることができます。
We index data in these entities by storing indexes in separate MySQL
tables. If we want to index three properties in each entity, we will
have three MySQL tables - one for each index. If we want to stop using
an index, we stop writing to that table from our code and, optionally,
drop the table from MySQL. If we want a new index, we make a new MySQL
table for that index and run a process to asynchronously populate the
index without disrupting our live service.
我
々は、インデックスを別々のMySQLテーブルに格納することによって、これらの実体でデータにインデックスを付けます。我々が各々の実体で3つの特性に
インデックスを付けたいならば、我々は3つのMySQLテーブルを持っています -
各々のインデックスのための1。我々がインデックスを使用するのを止めたいならば、我々は我々のコードからそのテーブルに書くのを止めて、任意に、
MySQLからテーブルを落とします。我々が新しいインデックスが欲しいならば、我々はそのインデックスのために新しいMySQLテーブルを作って、我々
の有効なサービスを中断させることなく非同期でインデックスに住むためにプロセスを走らせます。
As a result, we end up having more tables than we had before, but
adding and removing indexes is easy. We have heavily optimized the
process that populates new indexes (which we call "The Cleaner") so
that it fills new indexes rapidly without disrupting the site. We can
store new properties and index them in a day's time rather than a
week's time, and we don't need to swap MySQL masters and slaves or do
any other scary operational work to make it happen.
そ
の結果、我々は結局我々が前に持ったより多くのテーブルを持っていることになります、しかし、インデックスを加えて、削除することは簡単です。我々は、そ
れがサイトを崩壊させることなく速く新しいインデックスを満たすように、新しいインデックス(それを我々は「Cleaner」と呼びます)に住むプロセス
を重く最適化しました。我々は新しい特性を保存することができて、一日の時間に一週間の時間よりむしろ彼らにインデックスを付けることができます、そし
て、我々はMySQLマスターと奴隷を交換する必要はないか、それを起こらせるために他のどの怖い操作上の仕事もしません。
Details
詳細
In MySQL, our entities are stored in a table that looks like this:
MySQLに、我々の実体は、これのように見えるテーブルに保管されます:
CREATE TABLE entities (
added_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
id BINARY(16) NOT NULL,
updated TIMESTAMP NOT NULL,
body MEDIUMBLOB,
UNIQUE KEY (id),
KEY (updated)
) ENGINE=InnoDB;
CREATE表実体(added_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY(ID BINARY(16) NOT NULL)は、TIMESTAMP NOT NULL、体MEDIUMBLOB、UNIQUE KEY(ID)を更新しました、
キー(更新されます)
)ENGINE=InnoDB;
The
added_id column is present because InnoDB stores data rows physically in primary key order. The
added_id
InnoDBが身体的にデータ列を主要な重要な順序に保管するので、コラムは存在します。 AUTO_INCREMENT
primary key ensures new entities are written sequentially on disk after
old entities, which helps for both read and write locality (new
entities tend to be read more frequently than old entities since
FriendFeed pages are ordered reverse-chronologically). Entity bodies
are stored as zlib-compressed, pickled Python dictionaries.
AUTO_INCREMENT
主要な鍵により確実に新しい実体が古い実体の後で順番に椎間板に書かれることになります。そして、それは両方の読込み及び書込み場所(新しい実体は、
FriendFeedページから古い実体がリバース年代順に命じられるよりしばしば読まれる傾向があります)のために助けます。実体体は、zlib圧縮し
た、塩漬けにしたパイソン辞書として保存されます。
Indexes are stored in separate tables. To create a new index, we
create a new table storing the attributes we want to index on all of
our database shards. For example, a typical entity in FriendFeed might
look like this:
イ
ンデックスは、別々のテーブルに格納されます。新しいインデックスを作成するために、我々は我々が我々のデータベース破片の全ての上でインデックスに欲し
い特質を保存している新しいテーブルをつくります。たとえば、FriendFeedの典型的実体は、これのように見えるかもしれません:
{
"id": "71f0c4d2291844cca2df6f486e96e37c",
"user_id": "f48b0440ca0c4f66991c4d5f6a078eaf",
"feed_id": "f48b0440ca0c4f66991c4d5f6a078eaf",
"title": "We just launched a new backend system for FriendFeed!",
"link": "http://friendfeed.com/e/71f0c4d2-2918-44cc-a2df-6f486e96e37c",
"published": 1235697046,
"updated": 1235697046,
}
{
「ID」:「71f0c4d2291844cca2df6f486e96e37c」、「user_id」:「f48b0440ca0c4f66991c4d5f6a078eaf」、「feed_id」:「f48b0440ca0c4f66991c4d5f6a078eaf」、「タイトル」:「我々は、ちょうどFriendFeedのために新しいバックエンドシステムを開始しました!」、「関連」:「http://friendfeed.com/e/71f0c4d2-2918-44cc-a2df-6f486e96e37c",は「公表しました」:1235697046(「更新される」):1235697046}
We want to index the
我々は、インデックスに欲しいです user_id
attribute of these entities so we can render a page of all the entities
a given user has posted. Our index table looks like this:
user_id
我々が所定のユーザーが掲示したすべての実体のページを提出することができるように、これらの実体のものと考えてください。我々のインデックステーブルは、これのように見えます:
CREATE TABLE index_user_id (
user_id BINARY(16) NOT NULL,
entity_id BINARY(16) NOT NULL UNIQUE,
PRIMARY KEY (user_id, entity_id)
) ENGINE=InnoDB;
CREATE表index_user_id(user_id BINARY(16) NOT NULL、entity_id BINARY(16) NOT NULL UNIQUE、PRIMARY KEY(user_id、entity_id))ENGINE=InnoDB;
Our datastore automatically maintains indexes on
your behalf, so to start an instance of our datastore that stores
entities like the structure above with the given indexes, you would
write (in Python):
我々のデータストアは自動的にインデックスのあなたのためで生活を維持するので、伝えられたインデックスで上記の構造のような実体を保存する我々のデータストアの例を始めるために、あなたは書くでしょう(パイソンで):
user_id_index = friendfeed.datastore.Index(
table="index_user_id", properties=["user_id"], shard_on="user_id")
datastore = friendfeed.datastore.DataStore(
mysql_shards=["127.0.0.1:3306", "127.0.0.1:3307"],
indexes=[user_id_index])
new_entity = {
"id": binascii.a2b_hex("71f0c4d2291844cca2df6f486e96e37c"),
"user_id": binascii.a2b_hex("f48b0440ca0c4f66991c4d5f6a078eaf"),
"feed_id": binascii.a2b_hex("f48b0440ca0c4f66991c4d5f6a078eaf"),
"title": u"We just launched a new backend system for FriendFeed!",
"link": u"http://friendfeed.com/e/71f0c4d2-2918-44cc-a2df-6f486e96e37c",
"published": 1235697046,
"updated": 1235697046,
}
datastore.put(new_entity)
entity = datastore.get(binascii.a2b_hex("71f0c4d2291844cca2df6f486e96e37c"))
entity = user_id_index.get_all(datastore, user_id=binascii.a2b_hex("f48b0440ca0c4f66991c4d5f6a078eaf"))
=friendfeed.datastore.DataStore、=friendfeed.datastore.Index user_id_index(table="index_user_id」、properties=‖[「user_id」]、shard_on="user_id」)データストア(mysql_shards=[「127.0.0.1:3306」、「127.0.0.1:3307」]、indexes=[user_id_index])
new_entity ={
「ID」:binascii.a2b_hex(「71f0c4d2291844cca2df6f486e96e37c」)、「user_id」:binascii.a2b_hex(「f48b0440ca0c4f66991c4d5f6a078eaf」)、「feed_id」:binascii.a2b_hex(「f48b0440ca0c4f66991c4d5f6a078eaf」)、「タイトル」:u"Weは、ちょうどFriendFeedのために新しいバックエンドシステムを開始しました!」、「関連」:u"http://friendfeed.com/e/71f0c4d2-2918-44cc-a2df-6f486e96e37c」(「発表される」):1235697046(「更新される」):1235697046}
datastore.put(new_entity)実体= datastore.get(binascii.a2b_hex(「71f0c4d2291844cca2df6f486e96e37c」))実体= user_id_index.get_all(データストア(user_id=binascii.a2b_hex(「f48b0440ca0c4f66991c4d5f6a078eaf」)))
The
Index class above looks for the
インデックス
上記のクラスは、探します user_id property in all entities and automatically maintains the index in the
user_id
すべての実体で資産、そして、自動的に中でインデックスを維持する index_user_id table. Since our database is sharded, the
index_user_id
テーブル。我々のデータベースがshardedされるので、 shard_on argument is used to determine which shard the index gets stored on (in this case,
shard_on
議論は、インデックスがどの破片を保存させるかについて確定するのに用いられます(この場合、 entity["user_id"] % num_shards).
実体[「user_id」]% num_shards
)。
You can query an index using the index instance (see
あなたはインデックス例を使っているインデックスについてたずねることができます(見てください user_id_index.get_all above). The datastore code does the "join" between the
user_id_index.get_all
より上に)。データストアコードは、「接点」をします index_user_id table and the
index_user_id
テーブル、そして、 entities table in Python, by first querying the
実体
パイソンでテーブル、たずねることことで index_user_id tables on all database shards to get a list of entity IDs and then fetching those entity IDs from the
index_user_id
実体IDとその時のリストをそれらの実体IDを取ってくるようにするすべてのデータベース破片の上でテーブル entities table.
実体
テーブル。
To add a new index, e.g., on the
例えば、新しいものを加えることはインデックスを付けますの上で link property, we would create a new table:
リンク
資産、我々は新しいテーブルをつくります:
CREATE TABLE index_link (
link VARCHAR(735) NOT NULL,
entity_id BINARY(16) NOT NULL UNIQUE,
PRIMARY KEY (link, entity_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE表index_link(関連VARCHAR(735) NULLでない、entity_id BINARY(16) NOT NULL UNIQUE、PRIMARY KEY(関連、entity_id))ENGINE=InnoDB DEFAULT CHARSET=utf8;
We would change our datastore initialization code to include this new index:
我々は、この新しいインデックスを含むために、我々のデータストア初期化コードを変えます:
user_id_index = friendfeed.datastore.Index(
table="index_user_id", properties=["user_id"], shard_on="user_id")
link_index = friendfeed.datastore.Index(
table="index_link", properties=["link"], shard_on="link")
datastore = friendfeed.datastore.DataStore(
mysql_shards=["127.0.0.1:3306", "127.0.0.1:3307"],
indexes=[user_id_index, link_index])
=friendfeed.datastore.DataStore、=friendfeed.datastore.Index user_id_index(table="index_user_id」、properties=‖[「user_id」]、shard_on="user_id」)=friendfeed.datastore.Index link_index(table="index_link」、properties=‖[「関連」]、shard_on="link」)データストア(mysql_shards=[「127.0.0.1:3306」、「127.0.0.1:3307」]、indexes=[user_id_index、link_index])
And we could populate the index asynchronously (even while serving live traffic) with:
そして、我々は非同期でインデックスに住むことができました‖(生の交通に供給している間さえ)で:
./rundatastorecleaner.py --index=index_link
./rundatastorecleaner.py-index=index_link
Consistency and Atomicity
一貫性と原子価
Since our database is sharded, and indexes for an entity can be
stored on different shards than the entities themselves, consistency is
an issue. What if the process crashes before it has written to all the
index tables?
我々のデータベースがshardedされる、そして、実体のためのインデックスが実体自体より異なる破片の上に格納されることができるので、一貫性は問題です。それがすべてのインデックステーブルに書く前にプロセスがクラッシュするならば、どうですか?
Building a transaction protocol was appealing to the most ambitious
of FriendFeed engineers, but we wanted to keep the system as simple as
possible. We decided to loosen constraints such that:
業務プロトコルを構築することはFriendFeedエンジニアで最も野心的なものに訴えていました、しかし、我々はシステムをできるだけ単純にしておきたかったです。我々は、制約を緩和することに決めました:
-
The property bag stored in the main
概して保存される資産バッグentitiestable is canonical
実体
テーブルは、聖職服です -
Indexes may not reflect the actual entity values
インデックスは、実際の実体価値を反映しないかもしれません
Consequently, we write a new entity to the database with the following steps:
従って、我々は以下のステップでデータベースに新しい実体を書きます:
-
Write the entity to the
実体を書くentitiestable, using the ACID properties of InnoDB
実体
InnoDBのACID特性を使用してテーブル -
Write the indexes to all of the index tables on all of the shards
破片の全ての上に、インデックステーブルの全てに、インデックスを書いてください
When we read from the index tables, we know they may not be accurate
(i.e., they may reflect old property values if writing has not finished
step 2). To ensure we don't return invalid entities based on the
constraints above, we use the index tables to determine which entities
to read, but we re-apply the query filters on the entities themselves
rather than trusting the integrity of the indexes:
我
々がインデックステーブルから読んだとき、我々は彼らが正確でないかもしれない(すなわち、書くことがステップ2を終えなかったならば、彼らは古い財産価
値を熟考するかもしれません)ということを知っています。我々が上の制約に基づく無効な実体を返さないことを確実とするために、我々はどの実体を読むべき
かについて決定するためにインデックステーブルを使用します、しかし、我々はインデックスの完全性を信頼することよりむしろ実体自体の上で質問フィルタを
再び用います:
-
Read the
読むentity_idfrom all of the index tables based on the query
entity_id
質問に基づくインデックステーブルの全てから -
Read the entities from the
実体を読むentitiestable from the given entity IDs
実体
所定の実体IDからのテーブル -
Filter (in Python) all of the entities that do not match the query conditions based on the actual property values
質問に実際の財産価値に基づく状況に合うものを見つけない実体の全てをフィルターに通してください(パイソンで)
To ensure that indexes are not missing perpetually and
inconsistencies are eventually fixed, the "Cleaner" process I mentioned
above runs continously over the entities table, writing missing indexes
and cleaning up old and invalid indexes. It cleans recently updated
entities first, so inconsistencies in the indexes get fixed fairly
quickly (within a couple of seconds) in practice.
イ
ンデックスが永久になくなっていない、そして、矛盾が結局固定されることを確実とするために、私が上で言及した「よりきれいな」プロセスは実体テーブルの
上にcontinouslyに動作します。そして、なくなったインデックスを記述して、古くて無効なインデックスをきれいにします。それは最初に最近更新
された実体をきれいにするので、インデックスの矛盾は実際にはかなり速く固定されます(二秒以内に)。
Performance
パフォーマンス
We have optimized our primary indexes quite a bit in this new
system, and we are quite pleased with the results. Here is a graph of
FriendFeed page view latency for the past month (we launched the new
backend a couple of days ago, as you can tell by the dramatic drop):
我
々はこの新しいシステムでかなり我々の主要なインデックスを最適化しました、そして、我々は結果に全く満足します。前月(あなたが劇的な低下によって話す
ことができるように、我々は二日前新しいバックエンドを開始しました)の間のFriendFeedページ表示待ち時間のグラフは、ここにあります:
In particular, the latency of our system is now remarkably stable,
even during peak mid-day hours. Here is a graph of FriendFeed page view
latency for the past 24 hours:
特に、ピークの昼の時間の間にさえ、我々のシステムの待ち時間は、現在著しく安定です。過去24時間のFriendFeedページ表示待ち時間のグラフは、ここにあります:
Compare this to one week ago:
これを1週前と比較してください:
The system has been really easy to work with so far. We have already
changed the indexes a couple of times since we deployed the system, and
we have started converting some of our biggest MySQL tables to use this
new scheme so we can change their structure more liberally going
forward.
シ
ステムは、ここまで働くのが本当に簡単でした。我々がシステムを展開した時から、我々は2回インデックスをすでに変えました、そして、我々が進んでより自
由に彼らの構造を変えることができるように、我々はこの新しい計画を使うために我々の最大のMySQLテーブルのいくつかを変え始めました。








